package etracer

import (
	"context"
	"errors"
	"net/url"
	"strconv"
	"strings"
	"time"

	"git.edaijia-inc.cn/edaijia/ego/elogger"

	"github.com/SkyAPM/go2sky"
	swgin "github.com/SkyAPM/go2sky-plugins/gin/v3"
	"github.com/SkyAPM/go2sky/propagation"
	"github.com/SkyAPM/go2sky/reporter"
	agent "github.com/SkyAPM/go2sky/reporter/grpc/language-agent"
	"github.com/gin-gonic/gin"

	"github.com/WJIAN/gorequest"
)

const (
	SW_REDIS              = 30
	SW_MYSQL              = 33
	SW_GOLANG_HTTP_CLIENT = 5005
)

type ETracerConfig struct {
	OAPServer string `ini:"oapserver"`
	Interval  int    `ini:"interval"`
}

type ETracer struct {
	Reporter *go2sky.Reporter
	Tracer   *go2sky.Tracer
}

var ETracerg *ETracer

func InitETracer(config *ETracerConfig, name string) error {
	if config.OAPServer == "" {
		return errors.New("OAPServer can not be empty!")
	}

	interval := config.Interval
	if interval <= 0 {
		interval = 2
	}

	re, err := reporter.NewGRPCReporter(config.OAPServer,
		reporter.WithCheckInterval(time.Duration(interval)*time.Second),
	)
	if err != nil {
		elogger.Error("ETracer create reporter error, ", err)
		return err
	}

	tracer, err := go2sky.NewTracer(name, go2sky.WithReporter(re))
	if err != nil {
		elogger.Error("ETracer create tracer error", err)
	}

	ETracerg = &ETracer{&re, tracer}

	return nil
}

func InitGinMiddleware(r *gin.Engine) {
	if ETracerg != nil && ETracerg.Tracer != nil {
		r.Use(swgin.Middleware(r, ETracerg.Tracer))
	} else {
		elogger.Error("failed to init gin middlewate, please init ETracer firstly")
	}
}

func parseContext(ctxIf interface{}) context.Context {
	if ginContext, ok := ctxIf.(*gin.Context); ok {
		if ginContext.Request != nil {
			return ginContext.Request.Context()
		}
	} else if ctx, ok := ctxIf.(context.Context); ok {
		return ctx
	}

	return nil
}

func ERequestSpanAround(ctxIf interface{}, req *gorequest.SuperAgent, process func() (gorequest.Response, error)) {
	if ctxIf == nil || ETracerg == nil ||
		ETracerg.Tracer == nil || req.Url == "" {
		process()
		return
	}

	ctx := parseContext(ctxIf)
	if ctx == nil {
		process()
		return
	}

	u, err := url.Parse(req.Url)
	if err != nil {
		process()
		return
	}

	span, err := ETracerg.Tracer.CreateExitSpan(
		ctx,
		u.Path,
		u.Host,
		func(header string) error {
			req.Set(propagation.Header, header)
			return nil
		},
	)
	if err != nil {
		process()
		return
	}
	defer span.End()

	tagUrl := req.Url
	if len(req.QueryData) != 0 {
		//record query data only
		tagUrl = tagUrl + "?" + req.QueryData.Encode()
	}

	span.SetComponent(SW_GOLANG_HTTP_CLIENT)
	span.Tag(go2sky.TagHTTPMethod, "GET")
	span.Tag(go2sky.TagURL, tagUrl)
	span.SetSpanLayer(agent.SpanLayer_Http)

	resp, err := process()

	if resp != nil {
		span.Tag(go2sky.TagStatusCode, strconv.Itoa(resp.StatusCode))
	}

	if err != nil {
		span.Error(time.Now(), err.Error())
	}
}

func ERedisSpanAround(ctxIf interface{}, peer string, op string, process func() error, args ...interface{}) {
	if ctxIf == nil || ETracerg == nil || ETracerg.Tracer == nil || len(args) == 0 {
		process()
		return
	}

	ctx := parseContext(ctxIf)
	if ctx == nil {
		process()
		return
	}

	span, err := ETracerg.Tracer.CreateExitSpan(
		ctx,
		op,
		peer,
		func(header string) error {
			return nil
		},
	)
	if err != nil {
		process()
		return
	}
	defer span.End()

	span.SetComponent(SW_REDIS)
	span.Tag(go2sky.TagDBType, "redis")
	//args[0] must be command
	command, ok := args[0].(string)
	if !ok {
		command = "UNKNOWN"
	} else {
		command = strings.ToUpper(command)
	}
	span.Tag(go2sky.TagDBStatement, command)
	if len(args) > 1 {
		redisKey, ok := args[1].(string)
		if ok {
			span.Tag("redis.key", redisKey)
		}
	}
	span.SetSpanLayer(agent.SpanLayer_Database)

	err = process()

	if err != nil {
		span.Error(time.Now(), err.Error())
	}
}

func EMysqlSpanStart(ctxIf interface{}, peer string, op string) go2sky.Span {
	if ctxIf == nil || ETracerg == nil || ETracerg.Tracer == nil {
		return nil
	}

	ctx := parseContext(ctxIf)
	if ctx == nil {
		return nil
	}

	span, err := ETracerg.Tracer.CreateExitSpan(
		ctx,
		op,
		peer,
		func(header string) error {
			return nil
		},
	)
	if err != nil {
		return nil
	}

	return span
}

func EMysqlSpanEnd(span go2sky.Span, sql string, err error) {
	if span == nil {
		return
	}
	span.SetComponent(SW_MYSQL)
	span.Tag(go2sky.TagDBType, "sql")
	span.Tag(go2sky.TagDBStatement, sql)
	span.SetSpanLayer(agent.SpanLayer_Database)

	if err != nil {
		span.Error(time.Now(), err.Error())
	}

	span.End()
}

//TODO 目前不支持嵌套span,如果需要嵌套要考虑ctx如何返回
func EFuncSpanAround(ctx context.Context, op string, process func() error) {
	if ctx == nil || ETracerg == nil || ETracerg.Tracer == nil {
		return
	}

	span, _, err := ETracerg.Tracer.CreateLocalSpan(
		ctx,
		go2sky.WithSpanType(go2sky.SpanTypeLocal),
		go2sky.WithOperationName(op),
	)
	if err != nil {
		process()
		return
	}
	defer span.End()

	err = process()

	if err != nil {
		span.Error(time.Now(), err.Error())
	}
}
